# This is an experimental cmakefile and doesn't offer all of the options
# available in the configure script!  At this time you should prefer to
# use the configure script to build and install watchman!
cmake_minimum_required(VERSION 3.0.2 FATAL_ERROR)
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake" ${CMAKE_MODULE_PATH})

# Tell CMake to also look in the directories where getdeps.py installs
# our third-party dependencies.
list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_SOURCE_DIR}/external/install")

enable_testing()
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${CMAKE_CURRENT_BINARY_DIR})
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/external/install/include")

set(PACKAGE_VERSION "4.9.4")
set(WATCHMAN_VERSION_OVERRIDE "" CACHE STRING "Use this version code for \
Watchman instead of the default (${PACKAGE_VERSION})")
if (WATCHMAN_VERSION_OVERRIDE)
  set(PACKAGE_VERSION "${WATCHMAN_VERSION_OVERRIDE}")
endif ()

set(PACKAGE_NAME      "watchman")
set(PACKAGE_STRING    "${PACKAGE_NAME} ${PACKAGE_VERSION}")
set(PACKAGE_TARNAME   "${PACKAGE_NAME}-${PACKAGE_VERSION}")
set(PACKAGE_BUGREPORT "https://github.com/facebook/watchman/issues")
project(${PACKAGE_NAME} CXX C)
include(CheckFunctionExists)
include(CheckIncludeFiles)
include(CheckStructHasMember)
include(CheckSymbolExists)

# configure_file wants us to define a separate file.  I'd rather not
# have boilerplate for the same thing in two difference files, so we
# roll the checks in together with writing out the features to config.h
# ourselves here.
function(config_h LINE)
  file(APPEND "${CMAKE_CURRENT_BINARY_DIR}/config.h.new" "${LINE}\n")
endfunction()

file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/config.h.new" "#pragma once\n")

config_h("// Generated by cmake")
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
  config_h("#define WATCHMAN_CONFIG_FILE \
\"C:/ProgramData/facebook/watchman.json\"")
else()
  config_h("#define WATCHMAN_CONFIG_FILE \"/etc/watchman.json\"")
  set(WATCHMAN_STATE_DIR "/var/run/watchman")
  config_h("#define WATCHMAN_STATE_DIR \"${WATCHMAN_STATE_DIR}\"")
endif()
config_h("#define PACKAGE_VERSION \"${PACKAGE_VERSION}\"")

# While most of these tests are not strictly needed on windows, it is vital
# that we probe for and find strtoll in order for the jansson build to use
# a 64-bit integer type, otherwise the mtime_us field renders as garbage
# in the integration tests.
foreach(wat_func
    FSEventStreamSetExclusionPaths
    accept4
    backtrace
    backtrace_symbols
    backtrace_symbols_fd
    fdopendir
    getattrlistbulk
    inotify_init
    inotify_init1
    kqueue
    localeconv
    memmem
    mkostemp
    openat
    pipe2
    port_create
    statfs
    strtoll
    sys_siglist
)
  CHECK_FUNCTION_EXISTS(${wat_func} have_${wat_func})
  if (have_${wat_func})
    string(TOUPPER have_${wat_func} sym)
    config_h("#define ${sym} 1")
  endif()
endforeach(wat_func)

foreach(wat_header
    CoreServices/CoreServices.h
    execinfo.h
    fcntl.h
    inttypes.h
    locale.h
    port.h
    sys/event.h
    sys/inotify.h
    sys/mount.h
    sys/param.h
    sys/resource.h
    sys/socket.h
    sys/statfs.h
    sys/statvfs.h
    sys/types.h
    sys/ucred.h
    sys/vfs.h
    valgrind/valgrind.h
    unistd.h
)
  CHECK_INCLUDE_FILES(${wat_header} have_${wat_header})
  if (have_${wat_header})
    string(TOUPPER have_${wat_header} sym)
    string(REGEX REPLACE [./] _ sym ${sym})
    config_h("#define ${sym} 1")
  endif()
endforeach(wat_header)

CHECK_STRUCT_HAS_MEMBER(statvfs f_fstypename sys/statvfs.h
  HAVE_STRUCT_STATVFS_F_FSTYPENAME)
if (HAVE_STRUCT_STATVFS_F_BASETYPE)
  config_h("define HAVE_STRUCT_STATVFS_F_FSTYPENAME 1")
endif()

CHECK_STRUCT_HAS_MEMBER(statvfs f_basetype sys/statvfs.h
  HAVE_STRUCT_STATVFS_F_BASETYPE)
if (HAVE_STRUCT_STATVFS_F_BASETYPE)
  config_h("define HAVE_STRUCT_STATVFS_F_BASETYPE 1")
endif()

if(have_fcntl.h)
  CHECK_SYMBOL_EXISTS(O_SYMLINK fcntl.h HAVE_DECL_O_SYMLINK)
  if(HAVE_DECL_O_SYMLINK)
    config_h("#define HAVE_DECL_O_SYMLINK 1")
  endif()
endif()
find_package(PCRE)
if(PCRE_FOUND)
  config_h("#define HAVE_PCRE_H 1")
endif()

# Now close out config.h.  We only want to touch the file if the contents are
# different, so do a little dance to figure that out.
if(EXISTS "${CMAKE_CURRENT_BINARY_DIR}/config.h")
  file(MD5 "${CMAKE_CURRENT_BINARY_DIR}/config.h" orig_hash)
  file(MD5 "${CMAKE_CURRENT_BINARY_DIR}/config.h.new" this_hash)
  if(NOT orig_hash STREQUAL this_hash)
    file(RENAME "${CMAKE_CURRENT_BINARY_DIR}/config.h.new"
      "${CMAKE_CURRENT_BINARY_DIR}/config.h")
  endif()
else()
  file(RENAME "${CMAKE_CURRENT_BINARY_DIR}/config.h.new"
    "${CMAKE_CURRENT_BINARY_DIR}/config.h")
endif()

configure_file(
  thirdparty/jansson/jansson_config.h.in
  thirdparty/jansson/jansson_config.h)
include_directories(${CMAKE_CURRENT_BINARY_DIR}/thirdparty/jansson)

set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)

# This block is for cmake 3.0 which doesn't define the Threads::Threads
# interface section.  Test for that and define it for ourselves.
if(THREADS_FOUND AND NOT TARGET Threads::Threads)
  add_library(Threads::Threads INTERFACE IMPORTED)

  if(THREADS_HAVE_PTHREAD_ARG)
    set_property(TARGET Threads::Threads PROPERTY
      INTERFACE_COMPILE_OPTIONS "-pthread")
  endif()

  if(CMAKE_THREAD_LIBS_INIT)
    set_property(TARGET Threads::Threads PROPERTY
      INTERFACE_LINK_LIBRARIES "${CMAKE_THREAD_LIBS_INIT}")
  endif()
endif()


find_package(OpenSSL)

# This block is for cmake 3.0 which doesn't define the OpenSSL::Crypto
# interface section.  Test for that and define it for ourselves.
if(OPENSSL_FOUND AND NOT TARGET OpenSSL::Crypto)
  add_library(OpenSSL::Crypto UNKNOWN IMPORTED)
    set_target_properties(OpenSSL::Crypto PROPERTIES
      INTERFACE_INCLUDE_DIRECTORIES "${OPENSSL_INCLUDE_DIR}")
    if(EXISTS "${OPENSSL_CRYPTO_LIBRARY}")
      set_target_properties(OpenSSL::Crypto PROPERTIES
        IMPORTED_LINK_INTERFACE_LANGUAGES "C"
        IMPORTED_LOCATION "${OPENSSL_CRYPTO_LIBRARY}")
    endif()
endif()

find_package(PythonInterp REQUIRED)

if(PYTHONINTERP_FOUND)
  set(PYOUT "${CMAKE_CURRENT_BINARY_DIR}/build/pytimestamp")
  set(PYSETUP "${CMAKE_CURRENT_SOURCE_DIR}/python/setup.py")
  add_custom_command(
    OUTPUT ${PYOUT}
    # DEPENDS "python/pywatchman/*.py"
    COMMAND ${PYTHON_EXECUTABLE} ${PYSETUP} clean
      build_py -c -d ${CMAKE_CURRENT_BINARY_DIR}/python
      build_ext -b ${CMAKE_CURRENT_BINARY_DIR}/python
                -t ${CMAKE_CURRENT_BINARY_DIR}/python/build/temp
  )
  add_custom_target(pybuild ALL DEPENDS ${PYOUT})
  install(CODE "execute_process(COMMAND ${PYTHON_EXECUTABLE} \
${PYSETUP} install --root ${CMAKE_INSTALL_PREFIX})")
endif()

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
  # Check target architecture
  if (NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
    message(FATAL_ERROR "watchman requires a 64bit target architecture.")
  endif()
  add_definitions(-D_CRT_SECURE_NO_WARNINGS=1)
  include_directories(${CMAKE_CURRENT_SOURCE_DIR}/winbuild)
  set(CMAKE_CXX_FLAGS "/Zi /Zo /MP /MT /Oi /EHsc /GL-")
  set(CMAKE_SHARED_LINKER_FLAGS
    "${CMAKE_SHARED_LINKER_FLAGS} /DEBUG /MT /OPT:NOREF")
  set(CMAKE_EXE_LINKER_FLAGS
    "${CMAKE_EXE_LINKER_FLAGS} /DEBUG /OPT:NOREF")
  set(CMAKE_MODULE_LINKER_FLAGS
    "${CMAKE_MODULE_LINKER_FLAGS} /DEBUG /MT /OPT:NOREF")
  set(CMAKE_STATIC_LIBRARY_FLAGS
    "${CMAKE_STATIC_LIBRARY_FLAGS} /DEBUG /MT /OPT:NOREF")
else()
  set(CMAKE_CXX_FLAGS_COMMON "-g -Wall -Wextra -std=gnu++14")
  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS_COMMON}")  # for cmake 3.0
  set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_COMMON}")
  set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_COMMON} -O3")
endif()

add_library(wildmatch STATIC
  thirdparty/wildmatch/wildmatch.c
  thirdparty/wildmatch/wildmatch.h
)
add_library(tap STATIC thirdparty/tap.cpp thirdparty/tap.h)
add_library(log STATIC PubSub.cpp log.cpp)
add_library(hash STATIC hash.cpp)
add_library(err STATIC root/poison.cpp root/warnerr.cpp)
add_library(jansson_utf STATIC thirdparty/jansson/utf.cpp)

add_library(string STATIC string.cpp)
target_link_libraries(string jansson_utf hash)

add_library(jansson STATIC
thirdparty/jansson/dump.cpp
thirdparty/jansson/error.cpp
thirdparty/jansson/load.cpp
thirdparty/jansson/memory.cpp
thirdparty/jansson/pack_unpack.cpp
thirdparty/jansson/strbuffer.cpp
thirdparty/jansson/strconv.cpp
thirdparty/jansson/value.cpp
)
target_link_libraries(jansson string)

list(APPEND testsupport_sources
ChildProcess.cpp
FileDescriptor.cpp
FileInformation.cpp
Pipe.cpp
ThreadPool.cpp
bser.cpp
cfg.cpp
expflags.cpp
ignore.cpp
opendir.cpp
pending.cpp
time.cpp
stream.cpp
)

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
  list(APPEND testsupport_sources
winbuild/asprintf.cpp
winbuild/time.cpp
winbuild/backtrace.cpp
winbuild/errmap.cpp
winbuild/pathmap.cpp
winbuild/posix_spawn.cpp
stream_win.cpp
  )
endif()

add_library(testsupport STATIC ${testsupport_sources})


target_link_libraries(testsupport log string jansson)

list(APPEND watchman_sources
ChildProcess.cpp
ContentHash.cpp
CookieSync.cpp
FileDescriptor.cpp
FileInformation.cpp
InMemoryView.cpp
LocalFileResult.cpp
Pipe.cpp
# PubSub.cpp  (in liblog)
QueryableView.cpp
SymlinkTargets.cpp
ThreadPool.cpp
bser.cpp
cfg.cpp
checksock.cpp
clientmode.cpp
clockspec.cpp
error_category.cpp
expflags.cpp
fstype.cpp
groups.cpp
# hash.cpp (in libhash)
ignore.cpp
ioprio.cpp
json.cpp
launchd.cpp
listener-user.cpp
listener.cpp
main.cpp
opendir.cpp
opt.cpp
pending.cpp
perf.cpp
sockname.cpp
spawn.cpp
state.cpp
stream.cpp
stream_stdout.cpp
# string.cpp (in libstring)
time.cpp
timedlock.cpp
tmp.cpp
query/base.cpp
query/dirname.cpp
query/empty.cpp
query/eval.cpp
query/fieldlist.cpp
query/glob.cpp
query/intcompare.cpp
query/match.cpp
query/name.cpp
query/parse.cpp
query/pcre.cpp
query/since.cpp
query/suffix.cpp
query/type.cpp
cmds/debug.cpp
cmds/find.cpp
# cmds/heapprof.cpp
cmds/info.cpp
cmds/log.cpp
cmds/query.cpp
cmds/reg.cpp
cmds/since.cpp
cmds/state.cpp
cmds/subscribe.cpp
cmds/trigger.cpp
cmds/watch.cpp
root/ageout.cpp
root/crawler.cpp
root/dir.cpp
root/file.cpp
root/init.cpp
root/iothread.cpp
root/notifythread.cpp
# root/poison.cpp (in liberr)
root/reap.cpp
root/resolve.cpp
root/stat.cpp
root/symlink.cpp
root/sync.cpp
root/threading.cpp
root/vcs.cpp
# root/warnerr.cpp (in liberr)
root/watchlist.cpp
saved_state/LocalSavedStateInterface.cpp
saved_state/SavedStateInterface.cpp
scm/Mercurial.cpp
scm/SCM.cpp
watcher/auto.cpp
# watcher/eden.cpp
watcher/fsevents.cpp
watcher/inotify.cpp
watcher/kqueue.cpp
watcher/portfs.cpp
)

if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
  list(APPEND watchman_sources
stream_win.cpp
watcher/win32.cpp
winbuild/errmap.cpp
winbuild/pathmap.cpp
winbuild/mkdir.cpp
winbuild/dir.cpp
winbuild/asprintf.cpp
winbuild/hostname.cpp
winbuild/time.cpp
winbuild/backtrace.cpp
winbuild/getopt_long.cpp
winbuild/posix_spawn.cpp
  )
else()
  list(APPEND watchman_sources
stream_unix.cpp
  )
endif()

add_executable(watchman ${watchman_sources})
target_link_libraries(watchman log hash string err jansson wildmatch)
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
  target_link_libraries(watchman "-framework CoreServices")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
  target_link_libraries(watchman shlwapi.lib advapi32.lib dbghelp.lib)
endif()
if(PCRE_FOUND)
  target_link_libraries(watchman ${PCRE_LIBRARY})
  target_include_directories(watchman PUBLIC ${PCRE_INCLUDE_DIR})
endif()


target_link_libraries(watchman Threads::Threads)
if(TARGET OpenSSL::Crypto)
  target_link_libraries(watchman OpenSSL::Crypto)
endif()

install(TARGETS watchman RUNTIME DESTINATION bin)

if(NOT CMAKE_SYSTEM_NAME STREQUAL "Windows")
  set(DEST_STATE_DIR "${CMAKE_INSTALL_PREFIX}${WATCHMAN_STATE_DIR}")
  install(DIRECTORY DESTINATION ${DEST_STATE_DIR} DIRECTORY_PERMISSIONS
    OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_WRITE
    GROUP_EXECUTE WORLD_READ WORLD_WRITE WORLD_EXECUTE SETGID)
endif()

set(tests)
# Helper function to define a unit test executable
function(t_test NAME)
  add_executable(${NAME}.t ${ARGN})
  target_link_libraries(${NAME}.t testsupport tap Threads::Threads wildmatch)
  if(TARGET OpenSSL::Crypto)
    target_link_libraries(${NAME}.t OpenSSL::Crypto)
  endif()
  if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    target_link_libraries(${NAME}.t shlwapi.lib advapi32.lib dbghelp.lib)
  endif()
  target_compile_definitions(${NAME}.t
    PUBLIC WATCHMAN_TEST_SRC_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}\")
  add_test(NAME ${NAME} COMMAND ${CMAKE_CURRENT_BINARY_DIR}/${NAME}.t)
  list(APPEND tests ${NAME}.t)
endfunction()

# The `check` target runs the unit tests
add_custom_target(check
  DEPENDS ${tests}
  COMMAND ${CMAKE_CTEST_COMMAND})

if(PYTHONINTERP_FOUND)

  if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    add_executable(susres winbuild/susres.cpp)
    add_custom_target(make_susres ALL DEPENDS susres)
  endif()

  # The `integration` target runs the unit tests and integration tests
  add_custom_target(integration
    DEPENDS pybuild check
    COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/runtests.py
      --watchman-path ${CMAKE_CURRENT_BINARY_DIR}/watchman
      --pybuild-dir ${CMAKE_CURRENT_BINARY_DIR}/python
      WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endif()

t_test(art tests/art_test.cpp tests/log_stub.cpp)
t_test(ignore tests/ignore_test.cpp tests/log_stub.cpp)
t_test(pending tests/pending_test.cpp tests/log_stub.cpp)
t_test(string tests/string_test.cpp tests/log_stub.cpp)
t_test(log tests/log.cpp)
t_test(bser tests/bser.cpp tests/log_stub.cpp)
t_test(wildmatch tests/wildmatch_test.cpp tests/log_stub.cpp)
t_test(childproc tests/childproc.cpp tests/log_stub.cpp)
t_test(result tests/ResultTest.cpp tests/log_stub.cpp)
t_test(optional tests/OptionalTest.cpp tests/log_stub.cpp)
t_test(future tests/FutureTest.cpp tests/log_stub.cpp)
t_test(cache tests/CacheTest.cpp tests/log_stub.cpp)
t_test(MapUtilTest tests/MapUtilTest.cpp tests/log_stub.cpp)
